<style lang="scss">
.ua-lazy-media {
  display: inline-block !important;
  position: relative;
  box-sizing: border-box;
  overflow: hidden;
  line-height: 1;
  &-border {
    border: 1px solid #e2e2e2;
  }
  & > video {
    width: 100%;
    height: 100%;
  }
  &--poster {
    width: 100%;
    height: 100%;
    overflow: hidden;
    z-index: 0;
    position: absolute;
    top: 0;
    left: 0;
  }
  &--progress {
    width: 100%;
    height: 0;
    position: absolute;
    left: 0;
    bottom: 0;
    opacity: 0;
    transition: height 0.5s ease 0.2s, opacity 0.3s ease 0.3s;
    &-thumb {
      position: absolute;
      top: 0;
      left: -100%;
      width: 100%;
      height: 100%;
    }
  }
  &:hover &--progress {
    height: 7px;
    opacity: 0.6;
    &:hover {
      height: 15px;
      opacity: 1;
      cursor: pointer;
    }
  }
  &--play {
    z-index: 2;
    cursor: pointer;
    @include translate-center(both);
  }
  &--hover {
    opacity: 0;
    transition: opacity 0.3s ease 0.2s;
  }
  &:hover &--hover {
    opacity: 1;
  }
  &--fullscreen {
    position: absolute;
    top: 0;
    right: 0;
    z-index: 2;
  }
}
</style>

<template>
  <div
    class="ua-lazy-media"
    :class="{ 'ua-lazy-media-border': border }"
    :style="boxStyle"
  >
    <audio
      v-if="media === 'audio'"
      ref="mediaElem"
      @timeupdate="onTimeupdate"
      @ended="onEnded"
      @canplay="onCanplay"
      @play="onPlay"
      @pause="onPause"
      @error="onError"
    />
    <video
      v-if="media === 'video'"
      ref="mediaElem"
      @timeupdate="onTimeupdate"
      @ended="onEnded"
      @canplay="onCanplay"
      @play="onPlay"
      @pause="onPause"
      @error="onError"
    />
    <div class="ua-lazy-media--poster" v-if="showPoster">
      <UImage :src="poster" :width="width" :height="height">
        <template #error>
          <UIconfont name="image-error" :size="imgErrorSize" color="#e2e2e2" />
        </template>
      </UImage>
    </div>
    <div
      v-if="played"
      class="ua-lazy-media--progress"
      ref="progressElem"
      :style="{ backgroundColor: status > -1 ? '#e2e2e2' : '' }"
      @mousedown="onMousedown"
    >
      <div class="ua-lazy-media--progress-fullscreen">
        <UIconfont name="fullscreen" :size="10" />
      </div>
      <div
        class="ua-lazy-media--progress-thumb"
        :style="{
          transform: `translate3d(${played}%, 0, 0)`,
          backgroundColor: playIconColor,
        }"
      />
    </div>
    <div class="ua-lazy-media--play" :class="{ 'ua-lazy-media--hover': toolHover }">
      <UIconfont
        :size="playIconSize"
        :name="iconName"
        :cursor="iconCursor"
        :color="iconColor"
        @click="onToggle"
      />
    </div>
    <template v-if="media === 'video' && loaded">
      <div
        class="ua-lazy-media--fullscreen"
        :class="{ 'ua-lazy-media--hover': toolHover }"
      >
        <UIconfont
          name="fullscreen"
          :size="15"
          color="#e2e2e2"
          cursor="pointer"
          @click="onFullcreen"
        />
      </div>
    </template>
  </div>
</template>

<script lang="ts">
import { qiniuFullLink } from '@assets/utils/qiniu'
import { fileRead } from '@assets/utils/util'
import {
  computed,
  defineComponent,
  onMounted,
  PropType,
  Ref,
  ref,
  unref,
  watch,
} from 'vue'
import UIconfont from './UIconfont.vue'
import { ElMessageBox } from 'element-plus'
import UImage from './UImage.vue'

const enum MediaStatus {
  /** 未赋值 src */
  NOT_LOAD = 0,
  /** 资源加载失败 */
  ERROR = 1,
  /** 暂停 */
  PAUSED = 2,
  /** 播放 */
  PLAYED = 3,
}

export default defineComponent({
  name: 'UMediaLazy',
  components: { UIconfont, UImage },
  props: {
    src: {
      type: [String, File],
      default: '',
    },
    poster: {
      type: [String, File],
      default: '',
    },
    noPoster: {
      type: Boolean,
      default: false,
    },
    border: {
      type: Boolean,
      default: true,
    },
    width: {
      type: Number,
      default: 50,
    },
    height: {
      type: Number,
      default: 50,
    },
    playIconSize: {
      type: Number,
      default: 25,
    },
    playIconColor: {
      type: String,
      default: '#009688',
    },
    lazy: {
      type: Boolean,
      default: true,
    },
    media: {
      type: String as PropType<'audio' | 'video'>,
      default: 'video',
      values: ['audio', 'video'],
    },
  },
  setup(props) {
    let mousedown = false
    const unit = import.meta.env.VITE_LENGTH_UNIT
    /** 播放状态 */
    const status = ref(MediaStatus.NOT_LOAD)
    /** 播放进度 */
    const played = ref<number | string>(0)
    /** <audio> */
    const mediaElem = ref<any>(null) as Ref<HTMLMediaElement>
    const progressElem = ref<any>(null) as Ref<HTMLDivElement>
    /** 已将资源赋值给 <audio>，与 props.lazy 配合 */
    const loaded = ref(false)
    const iconCursor = computed(() => {
      // 不是懒加载 或 懒加载但已赋值
      if (!props.lazy || loaded.value) {
        switch (status.value) {
          case MediaStatus.ERROR:
            return 'not-allowed'
          case MediaStatus.NOT_LOAD:
            return 'auto'
          default:
            return 'pointer'
        }
      }
      // 懒加载 & 未赋值
      return props.src ? 'pointer' : 'auto'
    })
    const iconName = computed(() => {
      // 不是懒加载 或 懒加载但已赋值
      if (!props.lazy || loaded.value) {
        switch (status.value) {
          case MediaStatus.PLAYED:
            return 'stop'
          case MediaStatus.PAUSED:
            return 'play'
          default:
            return props.media === 'audio' ? 'music' : 'video'
        }
      }
      // 懒加载 & 未赋值
      return props.src ? 'play' : props.media === 'audio' ? 'music' : 'video'
    })
    const iconColor = computed(() => {
      // 不是懒加载 或 懒加载但已赋值
      if (!props.lazy || loaded.value) {
        switch (status.value) {
          case MediaStatus.ERROR:
            return '#F56C6C'
          case MediaStatus.NOT_LOAD:
            return '#e2e2e2'
          default:
            return props.playIconColor
        }
      }
      // 懒加载 & 未赋值
      return props.src ? props.playIconColor : '#e2e2e2'
    })

    const boxStyle = computed(() => ({
      width: `${props.width}${unit}`,
      height: `${props.height}${unit}`,
      borderColor: props.media === 'video' ? '#e2e2e2' : iconColor.value,
      backgroundColor:
        props.media === 'video' ? (props.src ? '#000' : '#f2f2f2') : '#f2f2f2',
    }))

    const toolHover = computed(() => {
      if (props.media === 'audio') return false
      if (!props.src) return false
      if (played.value) return true
      return false
    })

    const showPoster = computed(() => {
      if (props.noPoster) return false
      if (props.media === 'video') {
        if (played.value) return false
      }
      return true
    })

    const imgErrorSize = computed(
      () => Math.min(props.width ?? 0, props.height ?? 0) * 0.4
    )

    // 资源卸载
    function unload() {
      const elem = unref(mediaElem)
      if (!elem.paused) {
        elem.pause()
      }
      elem.src = ''
      loaded.value = false
      status.value = MediaStatus.NOT_LOAD
    }

    // 加载资源
    function load() {
      return new Promise<void>((resolve, reject) => {
        const nv = props.src
        if (nv instanceof File) {
          fileRead(nv, 'base64').then((base64) => {
            mediaElem.value.src = base64
            loaded.value = true
            resolve()
          }, reject)
        } else {
          if (nv) {
            mediaElem.value.src = qiniuFullLink(nv)
            loaded.value = true
          }
          resolve()
        }
      })
    }

    onMounted(() => {
      if (!props.lazy) load()
    })

    watch(() => props.media, unload)

    watch(
      () => props.src,
      () => {
        unload()
        if (!props.lazy) load()
      }
    )

    // audio 播放时间更新
    function onTimeupdate(e: Event) {
      if (mousedown) return
      const target = e.target as HTMLMediaElement
      if (target.duration && target.currentTime) {
        const val = target.currentTime / target.duration
        played.value = isNaN(val) ? 0 : (val * 100).toFixed(2)
      } else {
        played.value = 0
      }
    }

    // audio 播放结束
    function onEnded() {
      status.value = MediaStatus.PAUSED
      played.value = 0
    }

    // audio 资源加载失败
    function onError() {
      // 进行 <audio>.src = '' 式赋值时也会报错
      if (mediaElem.value.getAttribute('src')) {
        status.value = MediaStatus.ERROR
      }
      played.value = 0
    }

    // audio 资源加载到足以进行播放
    function onCanplay() {
      // 拖拽进度条时也会触发 oncanplay 事件
      if (status.value !== MediaStatus.PAUSED && status.value !== MediaStatus.PLAYED) {
        status.value = MediaStatus.PAUSED
      }
    }

    function onPlay() {
      status.value = MediaStatus.PLAYED
    }

    function onPause() {
      status.value = MediaStatus.PAUSED
    }

    /** 拖拽进度条 */
    function onMousedown(e: MouseEvent) {
      mousedown = true
      const { left, width } = progressElem.value.getBoundingClientRect()

      played.value = (((e.clientX - left) / width) * 100).toFixed(2)

      const mousemove = (ev: MouseEvent) => {
        played.value = (((ev.clientX - left) / width) * 100).toFixed(2)
      }

      const mouseup = () => {
        mousedown = false
        const currentTime = (Number(played.value) / 100) * mediaElem.value.duration
        if (!isNaN(currentTime)) {
          mediaElem.value.currentTime = currentTime
        }
        document.removeEventListener('mousemove', mousemove)
        document.removeEventListener('mouseup', mouseup)
      }

      document.addEventListener('mousemove', mousemove)
      document.addEventListener('mouseup', mouseup)
    }

    /** 播放/暂停 */
    function onToggle() {
      if (!props.src) return

      const elem = unref(mediaElem)

      // 暂停状态
      if (elem.paused) {
        // 有资源。https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLMediaElement/readyState
        if (elem.readyState >= 2) {
          elem.play() // 播放
        } else {
          if (!loaded.value && props.src) {
            // 加载资源
            load().then(() => {
              if (loaded.value) {
                function play() {
                  if (!elem.getAttribute('src') || status.value === MediaStatus.ERROR)
                    return
                  if (status.value === MediaStatus.PAUSED) {
                    elem.play() // 播放
                  } else {
                    requestAnimationFrame(play)
                  }
                }
                play()
              }
            })
          }
        }
      } else {
        elem.pause() // 暂停
      }
    }

    function onFullcreen() {
      mediaElem.value.requestFullscreen().catch((error: TypeError) =>
        ElMessageBox({
          type: 'error',
          title: 'Request Fullscreen Error',
          message: error.message,
        })
      )
    }

    return {
      status,
      played,
      imgErrorSize,
      mediaElem,
      progressElem,
      boxStyle,
      iconName,
      iconCursor,
      iconColor,
      loaded,
      showPoster,
      toolHover,
      onTimeupdate,
      onEnded,
      onError,
      onCanplay,
      onPlay,
      onPause,
      onMousedown,
      onToggle,
      onFullcreen,
    }
  },
})
</script>
